using System.Text;

namespace SpirV;

public struct ModuleHeader
{
	public Version Version { get; set; }
	public string GeneratorVendor { get; set; }
	public string? GeneratorName { get; set; }
	public int GeneratorVersion { get; set; }
	public uint Bound { get; set; }
	public uint Reserved { get; set; }
}

[Flags]
public enum DisassemblyOptions
{
	None,
	ShowTypes,
	ShowNames,
	Default = ShowTypes | ShowNames
}

public class Disassembler
{
	public string Disassemble(Module module)
	{
		return Disassemble(module, DisassemblyOptions.Default);
	}

	public string Disassemble(Module module, DisassemblyOptions options)
	{
		m_sb.AppendLine("; SPIR-V");
		m_sb.Append("; Version: ").Append(module.Header.Version).AppendLine();
		if (module.Header.GeneratorName == null)
		{
			m_sb.Append("; Generator: unknown; ").Append(module.Header.GeneratorVersion).AppendLine();
		}
		else
		{
			m_sb.Append("; Generator: ").Append(module.Header.GeneratorVendor).Append(' ').
				Append(module.Header.GeneratorName).Append("; ").Append(module.Header.GeneratorVersion).AppendLine();
		}
		m_sb.Append("; Bound: ").Append(module.Header.Bound).AppendLine();
		m_sb.Append("; Schema: ").Append(module.Header.Reserved).AppendLine();

		string[] lines = new string[module.Instructions.Count + 1];
		lines[0] = m_sb.ToString();
		m_sb.Clear();

		for (int i = 0; i < module.Instructions.Count; i++)
		{
			ParsedInstruction instruction = module.Instructions[i];
			PrintInstruction(m_sb, instruction, options);
			lines[i + 1] = m_sb.ToString();
			m_sb.Clear();
		}

		int longestPrefix = 0;
		for (int i = 0; i < lines.Length; i++)
		{
			string line = lines[i];
			longestPrefix = Math.Max(longestPrefix, line.IndexOf('='));
			if (longestPrefix > 50)
			{
				longestPrefix = 50;
				break;
			}
		}

		m_sb.Append(lines[0]);
		for (int i = 1; i < lines.Length; i++)
		{
			string line = lines[i];
			int index = line.IndexOf('=');
			if (index == -1)
			{
				m_sb.Append(' ', longestPrefix + 4);
				m_sb.Append(line);
			}
			else
			{
				int pad = Math.Max(0, longestPrefix - index);
				m_sb.Append(' ', pad);
				m_sb.Append(line, 0, index);
				m_sb.Append('=');
				m_sb.Append(line, index + 1, line.Length - index - 1);
			}
			m_sb.AppendLine();
		}

		string result = m_sb.ToString();
		m_sb.Clear();
		return result;
	}

	private static void PrintInstruction(StringBuilder sb, ParsedInstruction instruction, DisassemblyOptions options)
	{
		if (instruction.Operands.Count == 0)
		{
			sb.Append(instruction.Instruction.Name);
			return;
		}

		int currentOperand = 0;
		if (instruction.Instruction.Operands[currentOperand].Type is IdResultType)
		{
			if (options.HasFlag(DisassemblyOptions.ShowTypes))
			{
				Type resultType = instruction.ResultType ?? throw new NullReferenceException();
				resultType.ToString(sb).Append(' ');
			}
			++currentOperand;
		}

		if (currentOperand < instruction.Operands.Count && instruction.Instruction.Operands[currentOperand].Type is IdResult)
		{
			if (!options.HasFlag(DisassemblyOptions.ShowNames) || string.IsNullOrWhiteSpace(instruction.Name))
			{
				PrintOperandValue(sb, instruction.Operands[currentOperand].Value, options);
			}
			else
			{
				sb.Append(instruction.Name);
			}
			sb.Append(" = ");

			++currentOperand;
		}

		sb.Append(instruction.Instruction.Name);
		sb.Append(' ');

		for (; currentOperand < instruction.Operands.Count; ++currentOperand)
		{
			PrintOperandValue(sb, instruction.Operands[currentOperand].Value, options);
			sb.Append(' ');
		}
	}

	private static void PrintOperandValue(StringBuilder sb, object value, DisassemblyOptions options)
	{
		switch (value)
		{
			case System.Type t:
				sb.Append(t.Name);
				break;

			case string s:
				{
					sb.Append('"');
					sb.Append(s);
					sb.Append('"');
				}
				break;

			case ObjectReference or:
				{
					if (options.HasFlag(DisassemblyOptions.ShowNames) && or.Reference != null && !string.IsNullOrWhiteSpace(or.Reference.Name))
					{
						sb.Append(or.Reference.Name);
					}
					else
					{
						or.ToString(sb);
					}
				}
				break;

			case IBitEnumOperandValue beov:
				PrintBitEnumValue(sb, beov, options);
				break;

			case IValueEnumOperandValue veov:
				PrintValueEnumValue(sb, veov, options);
				break;

			case VaryingOperandValue varOpVal:
				varOpVal.ToString(sb);
				break;

			default:
				sb.Append(value);
				break;
		}
	}

	private static void PrintBitEnumValue(StringBuilder sb, IBitEnumOperandValue enumOperandValue, DisassemblyOptions options)
	{
		foreach (uint key in enumOperandValue.Values.Keys)
		{
			sb.Append(enumOperandValue.GetEnumName(key));
			IReadOnlyList<object> value = enumOperandValue.Values[key];
			if (value.Count != 0)
			{
				sb.Append(' ');
				foreach (object v in value)
				{
					PrintOperandValue(sb, v, options);
				}
			}
		}
	}

	private static void PrintValueEnumValue(StringBuilder sb, IValueEnumOperandValue valueOperandValue, DisassemblyOptions options)
	{
		sb.Append(valueOperandValue.Key);
		if (valueOperandValue.Value is IList<object> valueList && valueList.Count > 0)
		{
			sb.Append(' ');
			foreach (object v in valueList)
			{
				PrintOperandValue(sb, v, options);
			}
		}
	}

	private readonly StringBuilder m_sb = new StringBuilder();
}
